Conversation
test(api): freeze legacy wire contract before native API split
Phase N-0 of the Native API plan: adds table-driven regression tests pinning the current REST and WebSocket wire shapes so the upcoming /api/v1/* work cannot drift the legacy surface accidentally.
REST contract tests cover: /api/call-upload (all three legacy API-key transports: X-API-Key header, ?key= query, key= form), /api/trunk-recorder-call-upload alias, test=1 connectivity check, /api/calls envelope, /api/calls/:id/audio, /api/calls/:id/transcript, /api/calls/:id/share CRUD, /api/shared/:token{,/audio}, /api/bookmarks{,/calls,POST}, /api/auth/{login,refresh,logout,password,me,tg-selection} including os_session/refresh_token cookie issuance and clearance, /api/admin/import/{talkgroups,units,groups,tags}, /api/admin/radioreference/preview/csv, /api/admin/transcriptions/status.
WS contract pins byte-exact JSON for every server-emitted legacy command constructor in internal/ws (CAL/CFG/VER/LSC/XPR/MAX/LFM/TRN, ADM_RES, ADM_RES error) plus a structural pin for ADM_EVT (volatile timestamp asserted as positive int64).
Tests-only; no production code changes. Skips CHANGELOG per project policy for pure internal refactor / regression-test additions.
Introduces the native API surface alongside the existing legacy routes,
without breaking any current consumer.
Routing & middleware:
- New /api/v1 route group with V1Marker() (sets apiVersion=v1 in the
gin context) and V1ErrorEnvelope() (rewrites legacy {"error":"..."}
responses into the native {"error":{"code","message","details"}}
envelope; already-native and 2xx responses pass through unchanged).
- shared.WriteAPIError + APIError/APIErrorResponse types with stable
string codes (validation_failed, unauthorized, forbidden, not_found,
conflict, unprocessable, rate_limited, internal). 5xx envelopes
inject details.requestId from the request-id middleware.
Auth:
- APIKeyAuth on v1 paths accepts ONLY Authorization: Bearer <api-key>;
legacy paths keep accepting X-API-Key, ?key=, and form key=.
- JWT-shaped Bearer values on v1 API-key routes are rejected with the
invalid_credentials envelope so clients surface the right error.
Endpoints:
- POST /api/v1/calls — native upload with field names systemId,
talkgroupId, startedAt (RFC 3339 only — unix timestamps rejected),
frequencyHz, durationMs, unitId. POST /api/v1/calls/test returns 204.
- Listener: GET/PUT /api/v1/listener/tg-selection (renamed from
/api/auth/tg-selection), plus calls list/audio/transcript, share,
bookmarks, and unauth health/setup/auth endpoints.
- Admin: /api/v1/admin/{import/*, radioreference/preview (no /csv
suffix), transcriptions/status, docs/session}, all JWT+admin gated.
Tests: shared/errors_test.go and calls/v1_test.go cover the envelope
shape and v1-specific upload validation (including unix-startedAt
rejection).
Implements the native JSON-object framed WebSocket protocol alongside
the existing legacy 3-letter array-framed protocol on /ws, /api/ws,
and /api/admin/ws. The legacy paths are unchanged and remain
byte-identical (Phase N-0 contract tests still green).
Backend:
- New JSON message constructors in internal/ws/messages_v1.go for every
legacy command: connection.welcome (VER), scanner.config (CFG),
call.new (CAL), call.transcript (TRN), listener.count (LSC),
listener.feedMap.snapshot/update (LFM), session.expired (XPR),
connection.rejected (MAX), admin.event (ADM_EVT), admin.request
(ADM_REQ in), admin.response (ADM_RES out).
- Per-client protocol-version field; the hub stays single-implementation
and the per-client encoder picks legacy vs. v1 at connect time.
- Routes /api/v1/ws/listener and /api/v1/ws/admin registered on the
root router (NOT the v1 group) — V1ErrorEnvelope buffers HTTP bodies
and would corrupt the WebSocket upgrade.
- admin.response error envelope mirrors the REST error envelope
({code,message,details?}) so clients can share a discriminator.
Frontend:
- Listener WS client connects to /api/v1/ws/listener; admin WS client
connects to /api/v1/ws/admin.
- Discriminated-union types over the native message set; dispatch
switches on msg.type instead of array position.
- Outbound listener.feedMap.update and admin.request emit JSON objects.
Tests: messages_v1_test.go (13 tests) shape/byte-pin every native
constructor and exercise listener+admin v1 handlers end-to-end through
a real httptest server. Existing legacy contract tests
(messages_test.go, legacy_contract_test.go) remain green.
Adds RFC 8594 deprecation headers, structured per-request warn logs, and
an admin dashboard banner listing API keys that still hit the legacy
surface. No legacy behaviour changed beyond the additive headers.
Backend:
- middleware.Deprecated(successor, sunset) attaches Deprecation: true,
Sunset, Link rel=successor-version, and Cache-Control: no-store to
every legacy /api/* and legacy WS route.
- Per-request slog.Warn("legacy endpoint hit", method, path,
apiKeyIdent) — apiKeyIdent is the truncated identifier (set by
APIKeyAuth alongside apiKeyID), never the raw key.
- New /api/v1/admin/legacy-usage endpoint returning a 24h aggregate
({method, path, apiKeyIdent, count, lastSeen}) backed by an in-memory
ring buffer; no schema change.
Frontend:
- LegacyUsageBanner reads /api/v1/admin/legacy-usage on the admin
dashboard, polls every 60s, dismissable per session via
sessionStorage. Expandable details table with method, path, API key,
count, last-seen relative time.
Tests: middleware/deprecation_test.go covers header emission and the
ring buffer; legacyusage/handler_test.go covers the aggregate endpoint;
LegacyUsageBanner.test.tsx covers loading, empty, populated, and
dismissed states.
* feat(frontend): migrate REST/audio/SW to /api/v1
The frontend has been carrying legacy /api/* deprecation traffic since
the native v1 surface landed. Move every client-side caller over to v1
so production logs stop showing self-inflicted hits in the legacy-usage
report:
- RTK Query base URL flips to /api/v1
- /api/v1/listener/tg-selection (was /api/auth/tg-selection)
- /api/v1/admin/radioreference/preview (was /admin/radioreference/preview/csv)
- /api/v1/admin/legacy-usage (was /v1/admin/legacy-usage relative to /api)
- raw fetch() in main.tsx auth-recovery and ToolsPanel Swagger session
- audio download URLs in player.ts, BookmarksPanel, SearchPanel
- service worker passthrough regex accepts both /api/v1/{calls,shared}
and the legacy variants during the transition window
- vite dev proxy forwards /api/v1/ws WebSocket upgrades
Legacy /api/* routes remain available with deprecation headers for
external consumers; this only moves the embedded frontend.
All 201 vitest specs pass. tsc clean. Backend unchanged.
* fix(frontend): move no-control-regex disable to the right line
The directive was on the const declaration; ESLint reports it unused
because the actual regex literal sits on the next line. Move the
comment onto the .replace() call that owns the control-class regex.
- frontend/.npmrc: move virtual-store-dir to ~/.cache/pnpm-vstore so it survives /tmp wipes on container/codespace restarts (9p workspace mount still cannot host the vstore — copy mode + overlay path keeps both constraints satisfied). - .devcontainer/post-create.sh: pre-create the cache dir on fresh containers. - backend/internal/ws/client.go: drop dead buildCFGMessage helper (callers use buildCFGFrames).
The os_swagger session cookie was scoped to /api/admin/docs, but the admin Tools panel opens Swagger UI at /api/v1/admin/docs/index.html. The browser refused to send the cookie for that path so the docs middleware rejected the request with 'swagger session required'. Widen the cookie path to /api so it's delivered on both the legacy /api/admin/docs/* and v1 /api/v1/admin/docs/* routes.
frontend/.npmrc pinned virtual-store-dir to /home/vscode/.cache/... which exists in the dev container but not on GitHub Actions runners, breaking 'pnpm install --frozen-lockfile' with EACCES on /home/vscode. Move the override to the dev container's containerEnv as NPM_CONFIG_VIRTUAL_STORE_DIR so it only applies inside the container. CI runners now use pnpm's default node_modules/.pnpm location.
Swagger UI previously showed only POST /v1/calls, POST /v1/calls/test, and GET /v1/admin/legacy-usage because the rest of the v1 routes — auth, listener, setup, health, bookmarks, share, calls read paths, admin imports, RadioReference preview, transcription status, docs session — inherited only the legacy /api/* annotations from their handlers. Add a comma-separated v1-* tag and a second @router /v1/... line to each shared handler's swag block. swag emits both the legacy and v1 operations from one godoc block, so each appears under both tag sections in Swagger UI. Pure documentation change; routing and behavior unchanged. The legacy annotations will be removed in a future cleanup pass after the legacy routes are retired.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cuts v1.3.0 with the native /api/v1/* REST + WebSocket surface and follow-up fixes.
Highlights
Added
/api/v1/*REST surface with structured error envelope ({error:{code,message,details}}) and stable string codes.POST /api/v1/calls) with native multipart field names and RFC 3339startedAtenforcement./api/v1/ws/{listener,admin}./api/*route./api/v1/admin/legacy-usage+ dashboard banner surfacing 24-hour legacy-API usage.Changed
/api/v1/*for every REST and WS interaction.Authorization: Bearer; legacy headers/query/form key methods stay on legacy routes.Fixed
/apiso it's sent on the v1 docs URL the admin tools panel opens.audioEncodingPresetmatches the UI default (mp3_32k)./api/calls/:id/audio.See CHANGELOG.md for the full list.